/*
 * Copyright 2009, Team Mondegreen:
 * Brian Hsieh - bh2181@columbia.edu
 * Eugene Yao - eyy2102@columbia.edu
 * Jake Amonette - jaa2149@columbia.edu
 * Tal Giat - tg2235@colubia.edu
 * 
 * This file is part of AMFM (A Minimal Fractal Music Language), a language created for
 * fractal music as part of the Programming Languages and Translator class at Columbia university.
 *
 * For more details go to: http://code.google.com/p/amfm/
 * 
 * AMFM is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * at your option any later version.
 * 
 * AMFM is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with AMFM.  If not, see <http://www.gnu.org/licenses/>. 
 */

package com.myronmarston.music.transformers;

import com.myronmarston.music.MidiNote;
import com.myronmarston.music.Note;
import com.myronmarston.music.NoteList;
import com.myronmarston.music.settings.AmFmSelfSimilaritySettings;
import com.myronmarston.util.Fraction;

/**
 * Transformer that applies the self-similarity algorithm to the given NoteList. This can apply to the pitch,
 * rhythm and/or volume, depending on the SelfSimilaritySettings. Example: G5 A5 B5 G5 -> G5 A5 B5 G5, A5 B5
 * C6 A5, B5 C6 D6 B5, G5 A5 B5 G5. It also does the self similarity so the result length will be the number
 * of bars passed to the self similarity settings. It will also add a long rest at the beginning of the result
 * to pad till the start bar that was passed to the settings. The self similarity might generate result that
 * is longer than the provided bars length so if we pass the required length we will truncate notes till we
 * get to the desired length. We always truncate whole notes thus we might need to pad the end with a rest
 * (e.g. our next node to truncate is 1/2 note but we only have space for 1/4 note, so we will delete this
 * note and pad the last beat with a rest).
 * 
 * @author Jake
 */
public class AmFmSelfSimilarityTransformer implements Transformer {
  private AmFmSelfSimilaritySettings settings;

  /**
   * Constructor gets a settings to apply. Setting determine whether or not self-similarity is applied to the
   * pitch, rhythm and/or volume, when to start play (which bar) and for how many bars to play. Bars are 4/4
   * length this is constant and cannot be changed.
   * 
   * @param settings the self-similarity settings to use
   */
  public AmFmSelfSimilarityTransformer(AmFmSelfSimilaritySettings settings) {
    if (settings == null) {
      throw new IllegalArgumentException("settings cannot be null");
    }
    this.settings = settings;
  }

  /**
   * Gets the self-similarity settings to be used by this transformer. Guaranteed to never be null. The
   * settings determine whether or not self-similarity is applied to the pitch, rhythm and/or volume, when to
   * start play (which bar) and for how many bars to play. Bars are 4/4 length this is constant and cannot be
   * changed.
   * 
   * @return the self-similarity settings
   */
  public AmFmSelfSimilaritySettings getSettings() {
    return settings;
  }

  /**
   * Transform the input list of notes based on the definitions in the self similarity settings.
   * 
   * @param input a list of notes we want to apply the transformations on
   * @return the input list after we applied all the self-similarity transformation on it. Its length is as
   *         defined in the settings lengthInBars, and it might be padded with a rest whose length is startBar
   *         - 1 bars.
   * @throws IllegalArgumentException if input is null
   */
  public NoteList transform(NoteList input) {
    if (input == null) {
      throw new IllegalArgumentException("input cannot be null");
    }
    Fraction desiredLength = new Fraction(getSettings().getLengthInBars(), 1);
    NoteList result = null;
    if (!getSettings().selfSimilarityShouldBeAppliedToSomething()) {
      // there is no self-similarity, so just return the original input copied as much as needed
      result = generateUsingIdentitySimilarity(input, desiredLength);
    } else { // Self-similarities will be applied
      NoteList tempList = input;

      if (getSettings().isContinuousPlay()) { // Should we apply the self-similarity continuously till the
        // desired length is reached
        for (Fraction totalLengthToOutputDuration = desiredLength.dividedBy(tempList.getDuration()); totalLengthToOutputDuration
            .asDouble() > 1.0; totalLengthToOutputDuration = desiredLength.dividedBy(tempList.getDuration())) {
          tempList = transformOneLevel(input, tempList);
        }
        tempList = trancateToDesiredLength(tempList);
        result = padWithRestsTillStartBar(tempList);
      } else { // we should just apply it once and then duplicate the output till desired length is reached
        result = generateUsingIdentitySimilarity(transformOneLevel(input, tempList), desiredLength);

      }
    }
    if (result != null) {
      result.setInstrument(input.getInstrument()); // set output's instrument to original input one
    }
    return result;
  }

  /**
   * Generates output note list using the identity similarity on list of notes. Meaning, copying the list of
   * notes till the required length has been filled.
   * 
   * @param input list of notes to use for the identity similarity
   * @param desiredOutputLength the length of the output we need (in bars)
   * @return a list of notes that are repetition of input list of notes up to the totalLength (in bars)
   */
  private NoteList generateUsingIdentitySimilarity(NoteList input, Fraction desiredOutputLength) {
    Fraction exactTimesToClone = desiredOutputLength.dividedBy(input.getDuration());
    int wholeTimesToClone = (int) Math.ceil(exactTimesToClone.asDouble());
    NoteList clonedInput = input.clone();
    NoteList result = new NoteList();
    for (int i = 0; i < wholeTimesToClone; i++) {
      result.addAll(clonedInput);
    }
    result = trancateToDesiredLength(result);
    return padWithRestsTillStartBar(result);
  }

  /**
   * Convert the input note list to the one with the exact length in bars that was defined in the settings.
   * Truncating notes from the end as needed. It will never "break" a note so if the last thus we might need
   * to pad the end with a rest (e.g. our next node to truncate is 1/2 note but we only have space for 1/4
   * note, so we will delete this note and pad the last beat with a rest).
   * 
   * @param input a list of notes to truncate to the length in bars defined in the settings.
   * @return the input list of notes truncated to the length in bars defined in the settings.
   */
  private NoteList trancateToDesiredLength(NoteList input) {
    Fraction desiredLength = new Fraction(getSettings().getLengthInBars(), 1);
    Fraction inputLength = input.getDuration();
    Fraction lengthLeftToTruncate = inputLength.minus(desiredLength);

    switch (lengthLeftToTruncate.compareTo(0)) {
    case -1: // less than zero error
      throw new RuntimeException("Problem generating self-similarity, should never get here");
    case 0: // equal do nothing return input
      return input;
    case 1: // we actually need to truncate - iterate on input and truncate from the end
      NoteList result = input.clone();
      for (int i = result.size() - 1; i >= 0; i--) {
        Note lastNote = result.remove(i);
        lengthLeftToTruncate = lengthLeftToTruncate.minus(lastNote.getDuration());
        switch (lengthLeftToTruncate.compareTo(0)) {
        case -1: // this note make our output too short remove it and pad the reminder with a rest
          result.add(Note.createRest(lengthLeftToTruncate.dividedBy(-1)));
          return result;
        case 0: // just remove this note and return we got the desired length exactly
          return result;
        case 1:
          // do nothing continue with the loop
        }
      }
      throw new RuntimeException("Problem generating self-similarity, should never get here");
    }

    return input;
  }

  /**
   * Add rest whose length is our startBar -1, so the note list will actually start playing at startBar
   * 
   * @param input the list of notes to pad with a rest till start bar
   * @return a list of notes padded with a rest till start bar (if needed)
   */
  private NoteList padWithRestsTillStartBar(NoteList input) {
    if (getSettings().getStartBar() == 1) {
      return input;
    } else {
      Note restNote = Note.createRest(new Fraction(getSettings().getStartBar() - 1, 1));
      NoteList result = new NoteList(1 + input.size());
      result.add(restNote);
      result.addAll(input);
      return result;
    }
  }

  /**
   * Does transformation on the input one time, based on the original germ and the current list of notes
   * input. For example if the original germ has 4 notes, and the current input has 4 notes, the output will
   * be 16 notes long where: first 4 notes are the identity transformation, second 4 notes are based on the
   * relationship between note 1 and 2 in the original germ, third 3 notes based on the relationship between
   * notes 1 and 3 in original germ, and last 4 notes based on relationship between 1 and 4 in the original
   * germ. There is a special handling for rests when we do relationship between the first note and a rest we
   * just convert all the input notes to rest and if there is a rhythm manipulation we apply it to the result
   * (because pitch, and volume has no meaning on a rest). Also the first note of the germ is the first
   * non-rest note in the germ.
   * 
   * @param germ the original input to do the manipulations based on
   * @param input the list of notes we want to do manipulation on
   * @return a list of notes after we have applied all the self similarities on it once
   */
  private NoteList transformOneLevel(NoteList germ, NoteList input) {
    Note firstGermNote = germ.getFirstAudibleNote(); // the note we will compare against for the
    // self-similarity
    NoteList transformedList; // used to store the temporary results of the transformations
    NoteList output = new NoteList(germ.size() * input.size()); // the final output

    for (Note germNote : germ) {
      if (germNote.isRest()) {
        transformedList = new NoteList();

        // the rest one will be one complete copy of the germ, all enclosed in a rest,
        // so we need to set the first note flag on it
        Note restNote = Note.createRest(input.getDuration());
        restNote.setIsFirstNoteOfGermCopy(true);

        transformedList.add(restNote);
        transformedList = transformRhythm(transformedList, firstGermNote, germNote);
      } else {
        transformedList = transformPitch(input, firstGermNote, germNote);
        transformedList = transformRhythm(transformedList, firstGermNote, germNote);
        transformedList = transformVolume(transformedList, firstGermNote, germNote);
      }
      output.addAll(transformedList);
    }

    return output;
  }

  /**
   * Do volume transformation one time on the input based on the relationship of pitch between first note of
   * input and given note of input
   * 
   * @param input a list of notes to perform the volume similarity on
   * @param firstNote the first note of the input to compare to
   * @param inputNote a note from the input we compare to the first note
   * @return a list of notes after applying the self similarity on pitch based on the relationship between the
   *         first and the given note
   */
  private NoteList transformPitch(NoteList input, Note firstNote, Note inputNote) {
    if (!this.getSettings().isApplyToPitch())
      return input;

    // transpose the input to the correct octave...
    OctaveTransformer octaveTransformer = new OctaveTransformer(inputNote.getOctave() - firstNote.getOctave());
    NoteList octaveTransformedList = octaveTransformer.transform(input);

    // transpose the input to the correct pitch level...
    TransposeTransformer transposer = new TransposeTransformer(inputNote.getScaleStep()
        - firstNote.getScaleStep(), inputNote.getLetterNumber() - firstNote.getLetterNumber());
    NoteList transposedList = transposer.transform(octaveTransformedList);

    // set the segment chromatic adjustment on this note as necessary...
    int segmentChromaticAdjustment = inputNote.getChromaticAdjustment() - firstNote.getChromaticAdjustment();
    for (Note n : transposedList) {
      n.setSegmentChromaticAdjustment(segmentChromaticAdjustment);
    }

    return transposedList;
  }

  /**
   * Do volume transformation one time on the input based on the relationship of rhythm between first note of
   * input and given note of input
   * 
   * @param input a list of notes to perform the volume similarity on
   * @param firstNote the first note of the input to compare to
   * @param inputNote a note from the input we compare to the first note
   * @return a list of notes after applying the self similarity on rhythm based on the relationship between
   *         the first and the given note
   */
  private NoteList transformRhythm(NoteList input, Note firstNote, Note inputNote) {
    if (!this.getSettings().isApplyToRhythm())
      return input;

    // scale the rhythm...
    assert (inputNote.getDuration().compareTo(0) > 0) : inputNote.getDuration(); // we would get div-by-zero
    // below if the duration is
    // zero, and less than zero
    // is nonsensical
    RhythmicDurationTransformer rhythmScaler = new RhythmicDurationTransformer(firstNote.getDuration()
        .dividedBy(inputNote.getDuration()));
    return rhythmScaler.transform(input);
  }

  /**
   * Do volume transformation one time on the input based on the relationship of volume between first note of
   * input and given note of input
   * 
   * @param input a list of notes to perform the volume similarity on
   * @param firstNote the first note of the input to compare to
   * @param inputNote a note from the input we compare to the first note
   * @return a list of notes after applying the self similarity on volume based on the relationship between
   *         the first and the given note
   */
  private NoteList transformVolume(NoteList input, Note firstNote, Note inputNote) {
    if (!this.getSettings().isApplyToVolume())
      return input;

    int remainingVolumeRange = // get the above or below volume range based on the volume of the current note
    // relative to the first note
    (inputNote.getVolume() > firstNote.getVolume()) ? MidiNote.MAX_VELOCITY - firstNote.getVolume()
        : firstNote.getVolume() - MidiNote.MIN_VELOCITY;

    // if there's no volume range left to use, we have no way to scale it...
    if (remainingVolumeRange == 0)
      return input;

    // Our input note volume should not be 0, or we will get a scale factor of -1,
    // which our volume transformer does not allow. But the code that calls this
    // method should check for this, so this should never occur...
    assert inputNote.getVolume() != 0 : inputNote.getVolume();

    Fraction scaleFactor = new Fraction(inputNote.getVolume() - firstNote.getVolume(), remainingVolumeRange);
    VolumeTransformer volumeScaler = new VolumeTransformer(scaleFactor);
    return volumeScaler.transform(input);
  }
}
